<template>
  <span ref="countRef">{{ displayValue }}</span>
</template>
 
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { isNumber, isFunction } from 'lodash-es'
 
const props = defineProps({
  /**
   * 动画开始值 
   */
  startVal: {
    type: Number,
    default: 0,
    validator: value => isNumber(value)
  },
  /**
   * 动画结束值 
   */
  endVal: {
    type: Number,
    required: true,
    validator: value => isNumber(value)
  },
  /**
   * 动画持续时间（毫秒）
   */
  duration: {
    type: Number,
    default: 2000,
    validator: value => isNumber(value) && value > 0 
  },
  /**
   * 是否自动开始动画 
   */
  autoplay: {
    type: Boolean,
    default: true 
  },
  /**
   * 小数位数 
   */
  decimals: {
    type: Number,
    default: 0,
    validator: value => value >= 0
  },
  /**
   * 十进制分隔符 
   */
  decimal: {
    type: String,
    default: '.'
  },
  /**
   * 分隔符 
   */
  separator: {
    type: String,
    default: ','
  },
  /**
   * 数字前缀 
   */
  prefix: {
    type: String,
    default: ''
  },
  /**
   * 数字后缀 
   */
  suffix: {
    type: String,
    default: ''
  },
  /**
   * 是否使用缓动函数 
   */
  useEasing: {
    type: Boolean,
    default: true
  },
  /**
   * 缓动函数 
   */
  easingFn: {
    type: Function,
    default: (t, b, c, d) => {
      // 默认缓动函数 - easeOutQuart 
      return -c * ((t = t / d - 1) * t * t * t - 1) + b
    }
  }
})
 
const emit = defineEmits(['mounted', 'callback'])
 
const countRef = ref(null)
const localStartVal = ref(props.startVal) 
const printVal = ref(null)
const paused = ref(false)
const localDuration = ref(props.duration) 
const startTime = ref(null)
const remaining = ref(null)
const rAF = ref(null)
 
// 格式化数字 
const formatNumber = num => {
  const { decimals, decimal, separator, prefix, suffix } = props 
  let numStr = Number(num).toFixed(decimals)
  
  numStr = numStr.replace('.',  decimal)
  
  // 添加千位分隔符 
  if (separator) {
    const parts = numStr.split(decimal) 
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, separator)
    numStr = parts.join(decimal) 
  }
  
  return prefix + numStr + suffix 
}
 
const displayValue = ref(formatNumber(props.startVal)) 
 
// 缓动函数 
const easeOutQuart = (t, b, c, d) => {
  return -c * ((t = t / d - 1) * t * t * t - 1) + b
}
 
// 开始动画
const start = () => {
  localStartVal.value  = props.startVal 
  localDuration.value  = props.duration  
  paused.value  = false 
  startTime.value  = null
  rAF.value  = requestAnimationFrame(count)
}
 
// 暂停动画 
const pause = () => {
  cancelAnimationFrame(rAF.value) 
  paused.value  = true 
}
 
// 恢复动画 
const resume = () => {
  startTime.value  = null
  paused.value  = false
  rAF.value  = requestAnimationFrame(count)
}
 
// 重置动画
const reset = () => {
  startTime.value  = null 
  cancelAnimationFrame(rAF.value) 
  displayValue.value  = formatNumber(props.startVal) 
}
 
// 动画核心逻辑 
const count = (timestamp) => {
  if (!startTime.value)  startTime.value  = timestamp
  
  const progress = timestamp - startTime.value 
  const remainingTime = localDuration.value  - progress 
  
  if (props.useEasing)  {
    if (props.easingFn  && isFunction(props.easingFn))  {
      printVal.value  = props.easingFn( 
        progress,
        localStartVal.value, 
        props.endVal  - localStartVal.value, 
        localDuration.value 
      )
    } else {
      printVal.value  = easeOutQuart(
        progress,
        localStartVal.value, 
        props.endVal  - localStartVal.value, 
        localDuration.value 
      )
    }
  } else {
    printVal.value  =
      localStartVal.value  +
      (props.endVal  - localStartVal.value)  * (progress / localDuration.value) 
  }
  
  // 确保不会超过结束值
  if (progress < localDuration.value)  {
    displayValue.value  = formatNumber(printVal.value) 
    rAF.value  = requestAnimationFrame(count)
  } else {
    displayValue.value  = formatNumber(props.endVal) 
    emit('callback')
  }
  
  // 暂停时保存剩余时间
  if (paused.value)  {
    cancelAnimationFrame(rAF.value) 
    remaining.value  = remainingTime 
  }
}
 
// 监听props变化
watch(
  () => props.endVal, 
  (newVal, oldVal) => {
    if (newVal !== oldVal && props.autoplay)  {
      start()
    }
  }
)
 
// 组件挂载时 
onMounted(() => {
  if (props.autoplay)  {
    start()
  }
  emit('mounted')
})
 
// 组件卸载前
onBeforeUnmount(() => {
  cancelAnimationFrame(rAF.value) 
})
 
// 暴露方法给父组件
defineExpose({
  start,
  pause,
  resume,
  reset
})
</script>